home *** CD-ROM | disk | FTP | other *** search
- # -*- coding: utf-8 -*-
- #
- # (c) Copyright 2001-2008 Hewlett-Packard Development Company, L.P.
- #
- # This program is free software; you can redistribute it and/or modify
- # it under the terms of the GNU General Public License as published by
- # the Free Software Foundation; either version 2 of the License, or
- # (at your option) any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # You should have received a copy of the GNU General Public License
- # along with this program; if not, write to the Free Software
- # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
- #
- # ****************************************************************************
- #
- # Copyright (C) 2003-2004 Roger Binns <rogerb@rogerbinns.com>
- #
- # This program is free software; you can redistribute it and/or modify
- # it under the terms of the BitPim license as detailed in the LICENSE file.
- #
- # Code for reading and writing Vcard
- #
- # VCARD is defined in RFC 2425 and 2426
- #
- # Original author: Roger Binns <rogerb@rogerbinns.com>
- # Modified for HPLIP by: Don Welch
- #
-
- # Local
- from base.g import *
-
- # Std Lib
- import quopri
- import base64
- import codecs
- import cStringIO
- import re
- import StringIO
- import codecs
-
-
-
- _boms = []
- # 64 bit
- try:
- import encodings.utf_64
- _boms.append( (codecs.BOM64_BE, "utf_64") )
- _boms.append( (codecs.BOM64_LE, "utf_64") )
- except: pass
-
- # 32 bit
- try:
- import encodings.utf_32
- _boms.append( (codecs.BOM_UTF32, "utf_32") )
- _boms.append( (codecs.BOM_UTF32_BE, "utf_32") )
- _boms.append( (codecs.BOM_UTF32_LE, "utf_32") )
- except: pass
-
- # 16 bit
- try:
- import encodings.utf_16
- _boms.append( (codecs.BOM_UTF16, "utf_16") )
- _boms.append( (codecs.BOM_UTF16_BE, "utf_16_be") )
- _boms.append( (codecs.BOM_UTF16_LE, "utf_16_le") )
- except: pass
-
- # 8 bit
- try:
- import encodings.utf_8
- _boms.append( (codecs.BOM_UTF8, "utf_8") )
- except: pass
-
- # Work arounds for Apple
- _boms.append( ("\0B\0E\0G\0I\0N\0:\0V\0C\0A\0R\0D", "utf_16_be") )
- _boms.append( ("B\0E\0G\0I\0N\0:\0V\0C\0A\0R\0D\0", "utf_16_le") )
-
-
- # NB: the 32 bit and 64 bit versions have the BOM constants defined in Py 2.3
- # but no corresponding encodings module. They are here for completeness.
- # The order of above also matters since the first ones have longer
- # boms than the latter ones, and we need to be unambiguous
-
- _maxbomlen = max([len(bom) for bom,codec in _boms])
-
- def opentextfile(name):
- """This function detects unicode byte order markers and if present
- uses the codecs module instead to open the file instead with
- appropriate unicode decoding, else returns the file using standard
- open function"""
- #with file(name, 'rb') as f:
- f = file(name, 'rb')
- start = f.read(_maxbomlen)
- for bom,codec in _boms:
- if start.startswith(bom):
- # some codecs don't do readline, so we have to vector via stringio
- # many postings also claim that the BOM is returned as the first
- # character but that hasn't been the case in my testing
- return StringIO.StringIO(codecs.open(name, "r", codec).read())
- return file(name, "rtU")
-
-
- _notdigits = re.compile("[^0-9]*")
- _tendigits = re.compile("^[0-9]{10}$")
- _sevendigits = re.compile("^[0-9]{7}$")
-
-
- def phonenumber_normalise(n):
- # this was meant to remove the long distance '1' prefix,
- # temporary disable it, will be done on a phone-by-phone case.
- return n
- nums = "".join(re.split(_notdigits, n))
- if len(nums) == 10:
- return nums
-
- if len(nums) == 11 and nums[0] == "1":
- return nums[1:]
-
- return n
-
- def phonenumber_format(n):
- if re.match(_tendigits, n) is not None:
- return "(%s) %s-%s" % (n[0:3], n[3:6], n[6:])
- elif re.match(_sevendigits, n) is not None:
- return "%s-%s" %(n[:3], n[3:])
- return n
-
-
- def nameparser_formatsimplename(name):
- "like L{formatname}, except we use the first matching component"
- _fullname = nameparser_getfullname(name)
- if _fullname:
- return _fullname
- return name.get('nickname', "")
-
-
- def nameparser_getfullname(name):
- """Gets the full name, joining the first/middle/last if necessary"""
- if name.has_key("full"):
- return name["full"]
- return ' '.join([x for x in nameparser_getparts(name) if x])
-
-
- # See the following references for name parsing and how little fun it
- # is.
- #
- # The simple way:
- # http://cvs.gnome.org/lxr/source/evolution-data-server/addressbook/libebook/
- # e-name-western*
- #
- # The "proper" way:
- # http://cvs.xemacs.org/viewcvs.cgi/XEmacs/packages/xemacs-packages/mail-lib/mail-extr.el
- #
- # How we do it
- #
- # [1] The name is split into white-space seperated parts
- # [2] If there is only one part, it becomes the firstname
- # [3] If there are only two parts, they become first name and surname
- # [4] For three or more parts, the first part is the first name and the last
- # part is the surname. Then while the last part of the remainder starts with
- # a lower case letter or is in the list below, it is prepended to the surname.
- # Whatever is left becomes the middle name.
-
- lastparts = [ "van", "von", "de", "di" ]
-
- # I would also like to proudly point out that this code has no comment saying
- # "Have I no shame". It will be considered incomplete until that happens
-
- def nameparser_getparts_FML(name):
- n = name.get("full")
-
- # [1]
- parts = n.split()
-
- # [2]
- if len(parts) <= 1:
- return (n, "", "")
-
- # [3]
- if len(parts) == 2:
- return (parts[0], "", parts[1])
-
- # [4]
- f = [parts[0]]
- m = []
- l = [parts[-1]]
- del parts[0]
- del parts[-1]
-
- while len(parts) and (parts[-1][0].lower() == parts[-1][0] or parts[-1].lower() in lastparts):
- l = [parts[-1]]+l
- del parts[-1]
-
- m = parts
-
- # return it all
- return (" ".join(f), " ".join(m), " ".join(l))
-
-
- def nameparser_getparts_LFM(name):
- n = name.get("full")
-
- parts = n.split(',')
-
- if len(parts) <= 1:
- return (n, '', '')
-
- _last = parts[0]
- _first = ''
- _middle = ''
- parts = parts[1].split()
-
- if len(parts) >= 1:
- _first = parts[0]
-
- if len(parts) > 1:
- _middle = ' '.join(parts[1:])
-
- return (_first, _middle, _last)
-
-
- def nameparser_getparts(name):
- """Returns (first, middle, last) for name. If the part doesn't exist
- then a blank string is returned"""
-
- # do we have any of the parts?
- for i in ("first", "middle", "last"):
- if name.has_key(i):
- return (name.get("first", ""), name.get("middle", ""), name.get("last", ""))
-
- # check we have full. if not return nickname
- if not name.has_key("full"):
- return (name.get("nickname", ""), "", "")
-
- n = name.nameparser_get("full")
-
- if ',' in n:
- return nameparser_getparts_LFM(name)
-
- return nameparser_getparts_FML(name)
-
-
-
-
- class VFileException(Exception):
- pass
-
-
-
- class VFile:
- _charset_aliases = {
- 'MACINTOSH': 'MAC_ROMAN'
- }
-
- def __init__(self, source):
- self.source = source
- self.saved = None
-
-
- def __iter__(self):
- return self
-
-
- def next(self):
- # Get the next non-blank line
- while True: # python desperately needs do-while
- line = self._getnextline()
-
- if line is None:
- raise StopIteration()
-
- if len(line) != 0:
- break
-
- # Hack for evolution. If ENCODING is QUOTED-PRINTABLE then it doesn't
- # offset the next line, so we look to see what the first char is
- normalcontinuations = True
- colon = line.find(':')
- if colon > 0:
- s = line[:colon].lower().split(";")
-
- if "quoted-printable" in s or 'encoding=quoted-printable' in s:
- normalcontinuations = False
- while line[-1] == "=" or line[-2] == '=':
- if line[-1] == '=':
- i = -1
- else:
- i = -2
-
- nextl = self._getnextline()
- if nextl[0] in ("\t", " "): nextl = nextl[1:]
- line = line[:i]+nextl
-
- while normalcontinuations:
- nextline = self._lookahead()
-
- if nextline is None:
- break
-
- if len(nextline) == 0:
- break
-
- if nextline[0] != ' ' and nextline[0] != '\t':
- break
-
- line += self._getnextline()[1:]
-
- colon = line.find(':')
-
- if colon < 1:
- # some evolution vcards don't even have colons
- # raise VFileException("Invalid property: "+line)
- log.debug("Fixing up bad line: %s" % line)
-
- colon = len(line)
- line += ":"
-
- b4 = line[:colon]
- line = line[colon+1:].strip()
-
- # upper case and split on semicolons
- items = b4.upper().split(";")
-
- newitems = []
- if isinstance(line, unicode):
- charset = None
-
- else:
- charset = "LATIN-1"
-
- for i in items:
- # ::TODO:: probably delete anything preceding a '.'
- # (see 5.8.2 in rfc 2425)
- # look for charset parameter
- if i.startswith("CHARSET="):
- charset = i[8:] or "LATIN-1"
- continue
-
- # unencode anything that needs it
- if not i.startswith("ENCODING=") and not i=="QUOTED-PRINTABLE": # evolution doesn't bother with "ENCODING="
- # ::TODO:: deal with backslashes, being especially careful with ones quoting semicolons
- newitems.append(i)
- continue
-
- try:
- if i == 'QUOTED-PRINTABLE' or i == "ENCODING=QUOTED-PRINTABLE":
- # technically quoted printable is ascii only but we decode anyway since not all vcards comply
- line = quopri.decodestring(line)
-
- elif i == 'ENCODING=B':
- line = base64.decodestring(line)
- charset = None
-
- else:
- raise VFileException("unknown encoding: "+i)
-
- except Exception,e:
- if isinstance(e,VFileException):
- raise e
- raise VFileException("Exception %s while processing encoding %s on data '%s'" % (str(e), i, line))
-
- # ::TODO:: repeat above shenanigans looking for a VALUE= thingy and
- # convert line as in 5.8.4 of rfc 2425
- if len(newitems) == 0:
- raise VFileException("Line contains no property: %s" % (line,))
-
- # charset frigging
- if charset is not None:
- try:
- decoder = codecs.getdecoder(self._charset_aliases.get(charset, charset))
- line,_ = decoder(line)
- except LookupError:
- raise VFileException("unknown character set '%s' in parameters %s" % (charset, b4))
-
- if newitems == ["BEGIN"] or newitems == ["END"]:
- line = line.upper()
-
- return newitems, line
-
-
- def _getnextline(self):
- if self.saved is not None:
- line = self.saved
- self.saved = None
- return line
- else:
- return self._readandstripline()
-
-
- def _readandstripline(self):
- line = self.source.readline()
- if line is not None:
- if len(line) == 0:
- return None
-
- elif line[-2:] == "\r\n":
- return line[:-2]
-
- elif line[-1] == '\r' or line[-1] == '\n':
- return line[:-1]
-
- return line
-
-
- def _lookahead(self):
- assert self.saved is None
- self.saved = self._readandstripline()
- return self.saved
-
-
-
- class VCards:
- "Understands vcards in a vfile"
-
-
- def __init__(self, vfile):
- self.vfile = vfile
-
-
- def __iter__(self):
- return self
-
-
- def next(self):
- # find vcard start
- field = value = None
- for field,value in self.vfile:
- if (field,value) != (["BEGIN"], "VCARD"):
- continue
-
- found = True
- break
-
- if (field,value) != (["BEGIN"], "VCARD"):
- # hit eof without any BEGIN:vcard
- raise StopIteration()
-
- # suck up lines
- lines = []
- for field,value in self.vfile:
- if (field,value) != (["END"], "VCARD"):
- lines.append( (field,value) )
- continue
-
- break
-
- if (field,value) != (["END"], "VCARD"):
- raise VFileException("There is a BEGIN:VCARD but no END:VCARD")
-
- return VCard(lines)
-
-
-
- class VCard:
- "A single vcard"
-
- def __init__(self, lines):
- self._version = (2,0) # which version of the vcard spec the card conforms to
- self._origin = None # which program exported the vcard
- self._data = {}
- self._groups = {}
- self.lines = []
-
- # extract version field
- for f,v in lines:
- assert len(f)
-
- if f == ["X-EVOLUTION-FILE-AS"]: # all evolution cards have this
- self._origin = "evolution"
-
- if f[0].startswith("ITEM") and (f[0].endswith(".X-ABADR") or f[0].endswith(".X-ABLABEL")):
- self._origin = "apple"
-
- if len(v) and v[0].find(">!$_") > v[0].find("_$!<") >= 0:
- self.origin = "apple"
-
- if f == ["VERSION"]:
- ver = v.split(".")
- try:
- ver = [int(xx) for xx in ver]
- except ValueError:
- raise VFileException(v+" is not a valid vcard version")
-
- self._version = ver
- continue
-
- # convert {home,work}.{tel,label} to {tel,label};{home,work}
- # this probably dates from *very* early vcards
- if f[0] == "HOME.TEL":
- f[0:1] = ["TEL", "HOME"]
-
- elif f[0] == "HOME.LABEL":
- f[0:1] = ["LABEL", "HOME"]
-
- elif f[0] == "WORK.TEL":
- f[0:1] = ["TEL", "WORK"]
-
- elif f[0] == "WORK.LABEL":
- f[0:1] = ["LABEL", "WORK"]
-
- self.lines.append( (f,v) )
-
- self._parse(self.lines, self._data)
- self._update_groups(self._data)
-
-
- def getdata(self):
- "Returns a dict of the data parsed out of the vcard"
- return self._data
-
- def get(self, key, default=''):
- return self._data.get(key, default)
-
-
- def _getfieldname(self, name, dict):
- """Returns the fieldname to use in the dict.
-
- For example, if name is "email" and there is no "email" field
- in dict, then "email" is returned. If there is already an "email"
- field then "email2" is returned, etc"""
- if name not in dict:
- return name
- for i in xrange(2,99999):
- if name+`i` not in dict:
- return name+`i`
-
-
- def _parse(self, lines, result):
- for field,value in lines:
- if len(value.strip()) == 0: # ignore blank values
- continue
-
- if '.' in field[0]:
- f = field[0][field[0].find('.')+1:]
- else:
- f = field[0]
-
- t = f.replace("-", "_")
- func = getattr(self, "_field_"+t, self._default_field)
- func(field, value, result)
-
-
- def _update_groups(self, result):
- """Update the groups info """
- for k,e in self._groups.items():
- self._setvalue(result, *e)
-
-
- # fields we ignore
-
- def _field_ignore(self, field, value, result):
- pass
-
-
- _field_LABEL = _field_ignore # we use the ADR field instead
- _field_BDAY = _field_ignore # not stored in bitpim
- _field_ROLE = _field_ignore # not stored in bitpim
- _field_CALURI = _field_ignore # not stored in bitpim
- _field_CALADRURI = _field_ignore # variant of above
- _field_FBURL = _field_ignore # not stored in bitpim
- _field_REV = _field_ignore # not stored in bitpim
- _field_KEY = _field_ignore # not stored in bitpim
- _field_SOURCE = _field_ignore # not stored in bitpim (although arguably part of serials)
- _field_PHOTO = _field_ignore # contained either binary image, or external URL, not used by BitPim
-
-
- # simple fields
-
- def _field_FN(self, field, value, result):
- result[self._getfieldname("name", result)] = self.unquote(value)
-
-
- def _field_TITLE(self, field, value, result):
- result[self._getfieldname("title", result)] = self.unquote(value)
-
-
- def _field_NICKNAME(self, field, value, result):
- # ::TODO:: technically this is a comma seperated list ..
- result[self._getfieldname("nickname", result)] = self.unquote(value)
-
-
- def _field_NOTE(self, field, value, result):
- result[self._getfieldname("notes", result)] = self.unquote(value)
-
-
- def _field_UID(self, field, value, result):
- result["uid"] = self.unquote(value) # note that we only store one UID (the "U" does stand for unique)
-
-
- #
- # Complex fields
- #
-
- def _field_N(self, field, value, result):
- value = self.splitandunquote(value)
- familyname = givenname = additionalnames = honorificprefixes = honorificsuffixes = None
- try:
- familyname = value[0]
- givenname = value[1]
- additionalnames = value[2]
- honorificprefixes = value[3]
- honorificsuffixes = value[4]
- except IndexError:
- pass
-
- if familyname is not None and len(familyname):
- result[self._getfieldname("last name", result)] = familyname
-
- if givenname is not None and len(givenname):
- result[self._getfieldname("first name", result)] = givenname
-
- if additionalnames is not None and len(additionalnames):
- result[self._getfieldname("middle name", result)] = additionalnames
-
- if honorificprefixes is not None and len(honorificprefixes):
- result[self._getfieldname("prefix", result)] = honorificprefixes
-
- if honorificsuffixes is not None and len(honorificsuffixes):
- result[self._getfieldname("suffix", result)] = honorificsuffixes
-
-
- _field_NAME = _field_N # early versions of vcard did this
-
-
- def _field_ORG(self, field, value, result):
- value = self.splitandunquote(value)
- if len(value):
- result[self._getfieldname("organisation", result)] = value[0]
-
- for f in value[1:]:
- result[self._getfieldname("organisational unit", result)] = f
-
-
- _field_O = _field_ORG # early versions of vcard did this
-
-
- def _field_EMAIL(self, field, value, result):
- value = self.unquote(value)
- # work out the types
- types = []
- for f in field[1:]:
- if f.startswith("TYPE="):
- ff = f[len("TYPE="):].split(",")
- else:
- ff = [f]
-
- types.extend(ff)
-
- # the standard doesn't specify types of "home" and "work" but
- # does allow for random user defined types, so we look for them
- type = None
- for t in types:
- if t == "HOME":
- type="home"
-
- if t == "WORK":
- type="business"
-
- if t == "X400":
- return # we don't want no steenking X.400
-
- preferred = "PREF" in types
-
- if type is None:
- self._setvalue(result, "email", value, preferred)
- else:
- addr = {'email': value, 'type': type}
- self._setvalue(result, "email", addr, preferred)
-
-
- def _field_URL(self, field, value, result):
- # the standard doesn't specify url types or a pref type,
- # but we implement it anyway
- value = self.unquote(value)
- # work out the types
- types = []
- for f in field[1:]:
- if f.startswith("TYPE="):
- ff = f[len("TYPE="):].split(",")
- else:
- ff=[f]
-
- types.extend(ff)
-
- type = None
- for t in types:
- if t == "HOME":
- type="home"
-
- if t == "WORK":
- type="business"
-
- preferred = "PREF" in types
-
- if type is None:
- self._setvalue(result, "url", value, preferred)
- else:
- addr = {'url': value, 'type': type}
- self._setvalue(result, "url", addr, preferred)
-
-
- def _field_X_SPEEDDIAL(self, field, value, result):
- if '.' in field[0]:
- group = field[0][:field[0].find('.')]
- else:
- group = None
- if group is None:
- # this has to belong to a group!!
- #print 'speedial has no group'
- log.debug("speeddial has no group")
- else:
- self._setgroupvalue(result, 'phone', { 'speeddial': int(value) },
- group, False)
-
-
- def _field_TEL(self, field, value, result):
- value = self.unquote(value)
- # see if this is part of a group
- if '.' in field[0]:
- group = field[0][:field[0].find('.')]
- else:
- group = None
-
- # work out the types
- types = []
-
- for f in field[1:]:
- if f.startswith("TYPE="):
- ff = f[len("TYPE="):].split(",")
- else:
- ff = [f]
-
- types.extend(ff)
-
- # type munging - we map vcard types to simpler ones
- munge = { "BBS": "DATA", "MODEM": "DATA", "ISDN": "DATA", "CAR": "CELL",
- "PCS": "CELL" }
-
- types = [munge.get(t, t) for t in types]
-
- # reduce types to home, work, msg, pref, voice, fax, cell, video, pager, data
- types = [t for t in types if t in ("HOME", "WORK", "MSG", "PREF", "VOICE",
- "FAX", "CELL", "VIDEO", "PAGER", "DATA")]
-
- # if type is in this list and voice not explicitly mentioned then it is not a voice type
- antivoice = ["FAX", "PAGER", "DATA"]
-
- if "VOICE" in types:
- voice = True
-
- else:
- voice = True # default is voice
-
- for f in antivoice:
- if f in types:
- voice = False
- break
-
- preferred = "PREF" in types
-
- # vcard allows numbers to be multiple things at the same time, such as home voice, home fax
- # and work fax so we have to test for all variations
-
- # if neither work or home is specified, then no default (otherwise things get really complicated)
- iswork = False
- ishome = False
- if "WORK" in types:
- iswork = True
-
- if "HOME" in types:
- ishome = True
-
- if len(types) == 0 or types == ["PREF"]:
- iswork = True # special case when nothing else is specified
-
-
- value = phonenumber_normalise(value)
- if iswork and voice:
- self._setgroupvalue(result,
- "phone", {"type": "business", "number": value},
- group, preferred)
-
- if ishome and voice:
- self._setgroupvalue(result,
- "phone", {"type": "home", "number": value},
- group, preferred)
-
- if not iswork and not ishome and "FAX" in types:
- # fax without explicit work or home
- self._setgroupvalue(result,
- "phone", {"type": "fax", "number": value},
- group, preferred)
-
- else:
- if iswork and "FAX" in types:
- self._setgroupvalue(result, "phone",
- {"type": "business fax", "number": value},
- group, preferred)
-
- if ishome and "FAX" in types:
- self._setgroupvalue(result, "phone",
- {"type": "home fax", "number": value},
- group, preferred)
-
- if "CELL" in types:
- self._setgroupvalue(result,
- "phone", {"type": "cell", "number": value},
- group, preferred)
-
- if "PAGER" in types:
- self._setgroupvalue(result,
- "phone", {"type": "pager", "number": value},
- group, preferred)
-
- if "DATA" in types:
- self._setgroupvalue(result,
- "phone", {"type": "data", "number": value},
- group, preferred)
-
-
- def _setgroupvalue(self, result, type, value, group, preferred=False):
- """ Set value of an item of a group
- """
- if group is None:
- # no groups specified
- return self._setvalue(result, type, value, preferred)
-
- group_type = self._groups.get(group, None)
-
- if group_type is None:
- # 1st one of the group
- self._groups[group] = [type, value, preferred]
-
- else:
- if type != group_type[0]:
- log.debug('Group %s has different types: %s, %s' % (group, type,groups_type[0]))
-
- if preferred:
- group_type[2] = True
-
- group_type[1].update(value)
-
-
- def _setvalue(self, result, type, value, preferred=False):
- if type not in result:
- result[type] = value
- return
-
- if not preferred:
- result[self._getfieldname(type, result)] = value
- return
-
- # we need to insert our value at the begining
- values = [value]
-
- for suffix in [""]+range(2,99):
- if type+str(suffix) in result:
- values.append(result[type+str(suffix)])
- else:
- break
-
- suffixes = [""]+range(2,len(values)+1)
-
- for l in range(len(suffixes)):
- result[type+str(suffixes[l])] = values[l]
-
-
- def _field_CATEGORIES(self, field, value, result):
- # comma seperated just for fun
- values = self.splitandunquote(value, seperator=",")
- values = [v.replace(";", "").strip() for v in values] # semi colon is used as seperator in bitpim text field
- values = [v for v in values if len(v)]
- v = result.get('categories', None)
-
- if v:
- result['categories'] = ';'.join([v, ";".join(values)])
-
- else:
- result['categories'] = ';'.join(values)
-
-
- def _field_SOUND(self, field, value, result):
- # comma seperated just for fun
- values = self.splitandunquote(value, seperator=",")
- values = [v.replace(";", "").strip() for v in values] # semi colon is used as seperator in bitpim text field
- values = [v for v in values if len(v)]
- result[self._getfieldname("ringtones", result)] = ";".join(values)
-
-
- _field_CATEGORY = _field_CATEGORIES # apple use "category" which is not in the spec
-
-
- def _field_ADR(self, field, value, result):
- # work out the type
- preferred = False
- type = "business"
-
- for f in field[1:]:
- if f.startswith("TYPE="):
- ff = f[len("TYPE="):].split(",")
-
- else:
- ff = [f]
-
- for x in ff:
- if x == "HOME":
- type = "home"
- if x == "PREF":
- preferred = True
-
- value = self.splitandunquote(value)
- pobox = extendedaddress = streetaddress = locality = region = postalcode = country = None
- try:
- pobox = value[0]
- extendedaddress = value[1]
- streetaddress = value[2]
- locality = value[3]
- region = value[4]
- postalcode = value[5]
- country = value[6]
- except IndexError:
- pass
-
- addr = {}
-
- if pobox is not None and len(pobox):
- addr["pobox"] = pobox
-
- if extendedaddress is not None and len(extendedaddress):
- addr["street2"] = extendedaddress
-
- if streetaddress is not None and len(streetaddress):
- addr["street"] = streetaddress
-
- if locality is not None and len(locality):
- addr["city"] = locality
-
- if region is not None and len(region):
- addr["state"] = region
-
- if postalcode is not None and len(postalcode):
- addr["postalcode"] = postalcode
-
- if country is not None and len(country):
- addr["country"] = country
-
- if len(addr):
- addr["type"] = type
- self._setvalue(result, "address", addr, preferred)
-
-
- def _field_X_PALM(self, field, value, result):
- # handle a few PALM custom fields
- ff = field[0].split(".")
- f0 = ff[0]
-
- if len(ff) > 1:
- f1 = ff[1]
- else:
- f1 = ''
-
- if f0.startswith('X-PALM-CATEGORY') or f1.startswith('X-PALM-CATEGORY'):
- self._field_CATEGORIES(['CATEGORIES'], value, result)
-
- elif f0 == 'X-PALM-NICKNAME' or f1 == 'X-PALM-NICKNAME':
- self._field_NICKNAME(['NICKNAME'], value, result)
-
- else:
- log.debug("Ignoring PALM custom field: %s" % field)
-
-
- def _default_field(self, field, value, result):
- ff = field[0].split(".")
- f0 = ff[0]
-
- if len(ff) > 1:
- f1 = ff[1]
- else:
- f1 = ''
-
- if f0.startswith('X-PALM-') or f1.startswith('X-PALM-'):
- self._field_X_PALM(field, value, result)
- return
-
- elif f0.startswith("X-") or f1.startswith("X-"):
- log.debug("Ignoring custom field: %s" % field)
- return
-
- log.debug("No idea what to do with %s (%s)" % (field, value[:80]))
-
-
-
- def unquote(self, value):
- # ::TODO:: do this properly (deal with all backslashes)
- return value.replace(r"\;", ";") \
- .replace(r"\,", ",") \
- .replace(r"\n", "\n") \
- .replace(r"\r\n", "\r\n") \
- .replace("\r\n", "\n") \
- .replace("\r", "\n")
-
-
- def splitandunquote(self, value, seperator=";"):
- # also need a splitandsplitandunquote since some ; delimited fields are then comma delimited
-
- # short cut for normal case - no quoted seperators
- if value.find("\\"+seperator)<0:
- return [self.unquote(v) for v in value.split(seperator)]
-
- # funky quoting, do it the slow hard way
- res = []
- build = ""
- v = 0
- while v < len(value):
- if value[v] == seperator:
- res.append(build)
- build = ""
- v += 1
- continue
-
-
- if value[v] == "\\":
- build += value[v:v+2]
- v += 2
- continue
-
- build += value[v]
- v += 1
-
- if len(build):
- res.append(build)
-
- return [self.unquote(v) for v in res]
-
-
- def version(self):
- "Best guess as to vcard version"
- return self._version
-
-
- def origin(self):
- "Best guess as to what program wrote the vcard"
- return self._origin
-
-
- def __getitem__(self, item):
- return self._data[item]
-
- def __repr__(self):
- return repr(self._data)
-
-
- # The formatters return a string
- def myqpencodestring(value):
- """My own routine to do qouted printable since the builtin one doesn't encode CR or NL!"""
- return quopri.encodestring(value).replace("\r", "=0D").replace("\n", "=0A")
-
-
- def format_stringv2(value):
- """Return a vCard v2 string. Any embedded commas or semi-colons are removed."""
- return value.replace("\\", "").replace(",", "").replace(";", "")
-
-
- def format_stringv3(value):
- """Return a vCard v3 string. Embedded commas and semi-colons are backslash quoted"""
- return value.replace("\\", "").replace(",", r"\,").replace(";", r"\;")
-
-
- _string_formatters = (format_stringv2, format_stringv3)
-
-
- def format_binary(value):
- """Return base 64 encoded string"""
- # encodestring always adds a newline so we have to strip it off
- return base64.encodestring(value).rstrip()
-
-
- def _is_sequence(v):
- """Determine if v is a sequence such as passed to value in out_line.
- Note that a sequence of chars is not a sequence for our purposes."""
- return isinstance(v, (type( () ), type([])))
-
-
- def out_line(name, attributes, value, formatter, join_char=";"):
- """Returns a single field correctly formatted and encoded (including trailing newline)
-
- @param name: The field name
- @param attributes: A list of string attributes (eg "TYPE=intl,post" ). Usually
- empty except for TEL and ADR. You can also pass in None.
- @param value: The field value. You can also pass in a list of components which will be
- joined with join_char such as the 6 components of N
- @param formatter: The function that formats the value/components. See the
- various format_ functions. They will automatically ensure that
- ENCODING=foo attributes are added if appropriate"""
-
- if attributes is None:
- attributes = [] # ensure it is a list
- else:
- attributes = list(attributes[:]) # ensure we work with a copy
-
- if formatter in _string_formatters:
- if _is_sequence(value):
- qp = False
- for f in value:
- f = formatter(f)
- if myqpencodestring(f) != f:
- qp = True
- break
-
- if qp:
- attributes.append("ENCODING=QUOTED-PRINTABLE")
- value = [myqpencodestring(f) for f in value]
-
- value = join_char.join(value)
- else:
- value = formatter(value)
- # do the qp test
- qp = myqpencodestring(value) != value
- if qp:
- value = myqpencodestring(value)
- attributes.append("ENCODING=QUOTED-PRINTABLE")
- else:
- assert not _is_sequence(value)
- if formatter is not None:
- value = formatter(value) # ::TODO:: deal with binary and other formatters and their encoding types
-
- res = ";".join([name]+attributes)+":"
- res += _line_reformat(value, 70, 70-len(res))
- assert res[-1] != "\n"
-
- return res+"\n"
-
-
- def _line_reformat(line, width=70, firstlinewidth=0):
- """Takes line string and inserts newlines
- and spaces on following continuation lines
- so it all fits in width characters
-
- @param width: how many characters to fit it in
- @param firstlinewidth: if >0 then first line is this width.
- if equal to zero then first line is same width as rest.
- if <0 then first line will go immediately to continuation.
- """
- if firstlinewidth == 0:
- firstlinewidth = width
-
- if len(line) < firstlinewidth:
- return line
-
- res = ""
-
- if firstlinewidth > 0:
- res += line[:firstlinewidth]
- line = line[firstlinewidth:]
-
- while len(line):
- res += "\n "+line[:width]
- if len(line)<width:
- break
-
- line = line[width:]
-
- return res
-
- def out_names(vals, formatter, limit=1):
- res = ""
- for v in vals[:limit]:
- # full name
- res += out_line("FN", None, nameparser_formatsimplename(v), formatter)
- # name parts
- f,m,l = nameparser_getparts(v)
- res += out_line("N", None, (l,f,m,"",""), formatter)
- # nickname
- nn = v.get("nickname", "")
-
- if len(nn):
- res += out_line("NICKNAME", None, nn, formatter)
-
- return res
-
- # Apple uses wrong field name so we do some futzing ...
- def out_categories(vals, formatter, field="CATEGORIES"):
- cats = [v.get("category") for v in vals]
- if len(cats):
- return out_line(field, None, cats, formatter, join_char=",")
-
- return ""
-
-
- def out_categories_apple(vals, formatter):
- return out_categories(vals, formatter, field="CATEGORY")
-
-
- # Used for both email and urls. we don't put any limits on how many are output
- def out_eu(vals, formatter, field, bpkey):
- res = ""
- first = True
- for v in vals:
- val = v.get(bpkey)
- type = v.get("type", "")
-
- if len(type):
- if type == "business":
- type = "work" # vcard uses different name
-
- type = type.upper()
-
- if first:
- type = type+",PREF"
-
- elif first:
- type = "PREF"
-
- if len(type):
- type = ["TYPE="+type+["",",INTERNET"][field == "EMAIL"]] # email also has "INTERNET"
- else:
- type = None
-
- res += out_line(field, type, val, formatter)
- first = False
-
- return res
-
-
- def out_emails(vals, formatter):
- return out_eu(vals, formatter, "EMAIL", "email")
-
-
- def out_urls(vals, formatter):
- return out_eu(vals, formatter, "URL", "url")
-
- _out_tel_mapping = {
- 'home': 'HOME',
- 'office': 'WORK',
- 'cell': 'CELL',
- 'fax': 'FAX',
- 'pager': 'PAGER',
- 'data': 'MODEM',
- 'none': 'VOICE'
- }
-
-
- def out_tel(vals, formatter):
- # ::TODO:: limit to one type of each number
- phones = ['phone'+str(x) for x in ['']+range(2,len(vals)+1)]
- res = ""
- first = True
- idx = 0
-
- for v in vals:
- sp = v.get('speeddial', None)
-
- if sp is None:
- # no speed dial
- res += out_line("TEL",
- ["TYPE=%s%s" % (_out_tel_mapping[v['type']], ("", ",PREF")[first])],
- phonenumber_format(v['number']), formatter)
- else:
- res += out_line(phones[idx]+".TEL",
- ["TYPE=%s%s" % (_out_tel_mapping[v['type']], ("", ",PREF")[first])],
- phonenumber_format(v['number']), formatter)
- res += out_line(phones[idx]+".X-SPEEDDIAL", None, str(sp), formatter)
- idx += 1
- first = False
-
- return res
-
-
- # and addresses
- def out_adr(vals, formatter):
- # ::TODO:: limit to one type of each address, and only one org
- res = ""
- first = True
- for v in vals:
- o = v.get("company", "")
-
- if len(o):
- res += out_line("ORG", None, o, formatter)
-
- if v.get("type") == "home":
- type = "HOME"
- else:
- type = "WORK"
-
- type = "TYPE="+type+("", ",PREF")[first]
- res += out_line("ADR", [type], [v.get(k, "") for k in (None, "street2", "street", "city", "state", "postalcode", "country")], formatter)
- first = False
-
- return res
-
-
- def out_note(vals, formatter, limit=1):
- return "".join([out_line("NOTE", None, v["memo"], formatter) for v in vals[:limit]])
-
-
- # Sany SCP-6600 (Katana) support
- def out_tel_scp6600(vals, formatter):
- res = ""
- _pref = len(vals) > 1
-
- if _pref:
- s = "PREF,"
- else:
- s = ''
-
- for v in vals:
- res += out_line("TEL", s,
- ["TYPE=%s%s" % (s, _out_tel_mapping[v['type']])],
- phonenumber_format(v['number']), formatter)
-
- _pref = False
-
- return res
-
-
- def out_email_scp6600(vals, formatter):
- res = ''
- for _idx in range(min(len(vals), 2)):
- v = vals[_idx]
-
- if v.get('email', None):
- res += out_line('EMAIL', ['TYPE=INTERNET'],
- v['email'], formatter)
-
- return res
-
-
- def out_url_scp660(vals, formatter):
- if vals and vals[0].get('url', None):
- return out_line('URL', None, vals[0]['url'], formatter)
- return ''
-
-
- def out_adr_scp6600(vals, formatter):
- for v in vals:
- if v.get('type', None) == 'home':
- _type = 'HOME'
- else:
- _type = 'WORK'
- return out_line("ADR", ['TYPE=%s' % _type],
- [v.get(k, "") for k in (None, "street2", "street", "city", "state", "postalcode", "country")],
- formatter)
- return ''
-
-
- # This is the order we write things out to the vcard. Although
- # vCard doesn't require an ordering, it looks nicer if it
- # is (eg name first)
- _field_order = ("names", "wallpapers", "addresses", "numbers", "categories",
- "emails", "urls", "ringtones", "flags", "memos", "serials")
-
-
- def output_entry(entry, profile, limit_fields=None):
-
- # debug build assertion that limit_fields only contains fields we know about
- if __debug__ and limit_fields is not None:
- assert len([f for f in limit_fields if f not in _field_order]) == 0
-
- fmt = profile["_formatter"]
- io = cStringIO.StringIO()
- io.write(out_line("BEGIN", None, "VCARD", None))
- io.write(out_line("VERSION", None, profile["_version"], None))
-
- if limit_fields is None:
- fields = _field_order
- else:
- fields = [f for f in _field_order if f in limit_fields]
-
- for f in fields:
- if f in entry and f in profile:
- func = profile[f]
- # does it have a limit? (nice scary introspection :-)
- if "limit" in func.func_code.co_varnames[:func.func_code.co_argcount]:
- lines = func(entry[f], fmt, limit = profile["_limit"])
- else:
- lines = func(entry[f], fmt)
- if len(lines):
- io.write(lines)
-
- io.write(out_line("END", None, "VCARD", fmt))
- return io.getvalue()
-
-
- profile_vcard2 = {
- '_formatter': format_stringv2,
- '_limit': 1,
- '_version': "2.1",
- 'names': out_names,
- 'categories': out_categories,
- 'emails': out_emails,
- 'urls': out_urls,
- 'numbers': out_tel,
- 'addresses': out_adr,
- 'memos': out_note,
- }
-
- profile_vcard3 = profile_vcard2.copy()
- profile_vcard3['_formatter'] = format_stringv3
- profile_vcard3['_version'] = "3.0"
-
- profile_apple = profile_vcard3.copy()
- profile_apple['categories'] = out_categories_apple
-
- profile_full = profile_vcard3.copy()
- profile_full['_limit'] = 99999
-
- profile_scp6600 = profile_full.copy()
- del profile_scp6600['categories']
-
- profile_scp6600.update(
- { 'numbers': out_tel_scp6600,
- 'emails': out_email_scp6600,
- 'urls': out_url_scp660,
- 'addresses': out_adr_scp6600,
- })
-
- profiles = {
- 'vcard2': { 'description': "vCard v2.1", 'profile': profile_vcard2 },
- 'vcard3': { 'description': "vCard v3.0", 'profile': profile_vcard3 },
- 'apple': { 'description': "Apple", 'profile': profile_apple },
- 'fullv3': { 'description': "Full vCard v3.0", 'profile': profile_full},
- 'scp6600': { 'description': "Sanyo SCP-6600 (Katana)",
- 'profile': profile_scp6600 },
- }
-
-
-
-